2015年AliCrackMe学习Android逆向
看雪论坛作者ID:直木
1
AliCrackme_1
1、运行题目:MuMu模拟器
需要输入正确的密码:
2、反编译分析
首先对apk文件进行反编译,找到登录按钮所在Activity(MainActivity)的代码并分析:
table 是使用方法 getTableFromPic 得到的一张字符表;
3、泄漏 table 和 pw
方法一:ddms/adb logcat:
它们都会通过Log打印出来,如上图中红框所示。所以,我们可以利用 DDMS 或者 adb logcat 查看log 。通过在APP中随便输入字符串,点击“登录”按钮,就能从Logcat中查看到打印值。如下图所示,输入“123456”。
方法二:逆向分析
如果没有Log.d方法,该怎么办呢?前面已经分析出 table 和 pw 分别是通过方法 getTableFromPic 和getPwdFromPic得到的,那么就分析这两个方法。
它们都是通过对assets目录下的logo.png图片进行计算得到返回值。那么就可以创建一个Android项目,把logo.png拷贝到新项目的assets目录下。然后将getTableFromPic 和getPwdFromPic拷贝到AndroidTest目录下的ExampleInstrumentedTest文件中。
package com.lzx.ufo;
import android.content.Context;
import android.util.Log;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.io.InputStream;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.lzx.ufo", appContext.getPackageName());
Log.d("UFO======",getTableFromPic(appContext));
Log.d("UFO======",getPwdFromPic(appContext));
}
protected String getTableFromPic(Context appContext) {
InputStream is = null;
String value = "";
try {
is = appContext.getResources().getAssets().open("logo.png");
int lenght = is.available();
// Log.d("UFO","Table-lenght:"+lenght);
byte[] b = new byte[lenght];
is.read(b, 0, lenght);
byte[] data = new byte[768];
System.arraycopy(b, 89473, data, 0, 768);
String value2 = new String(data, "utf-8");
if (is == null) {
return value2;
}
try {
is.close();
return value2;
} catch (IOException e) {
return value2;
}
} catch (Exception e2) {
e2.printStackTrace();
if (is == null) {
return value;
}
try {
is.close();
return value;
} catch (IOException e3) {
return value;
}
} catch (Throwable th) {
if (is != null) {
try {
is.close();
} catch (IOException e4) {
}
}
}
return value;
}
protected String getPwdFromPic(Context appContext) {
InputStream is = null;
String value = "";
try {
is = appContext.getResources().getAssets().open("logo.png");
int lenght = is.available();
// Log.d("UFO","Pwd-lenght:"+lenght);
byte[] b = new byte[lenght];
is.read(b, 0, lenght);
byte[] data = new byte[18];
System.arraycopy(b, 91265, data, 0, 18);
String value2 = new String(data, "utf-8");
if (is == null) {
return value2;
}
try {
is.close();
return value2;
} catch (IOException e) {
return value2;
}
} catch (Exception e2) {
e2.printStackTrace();
if (is == null) {
return value;
}
try {
is.close();
return value;
} catch (IOException e3) {
return value;
}
} catch (Throwable th) {
if (is != null) {
try {
is.close();
} catch (IOException e4) {
}
}
}
return value;
}
}
4、编写脚本计算flag
方法一:拷贝题目中的解码方法,使用Java代码解题
public class Answer{
public static void main(String[] args){
String table = "一乙二十丁厂七卜人入八九几儿了力乃刀又三于干亏士工土才寸下大丈与万上小口巾山千乞川亿个勺久凡及夕丸么广亡门义之尸弓己已子卫也女飞刃习叉马乡丰王井开夫天无元专云扎艺木五支厅不太犬区历尤友匹车巨牙屯比互切瓦止少日中冈贝内水见午牛手毛气升长仁什片仆化仇币仍仅斤爪反介父从今凶分乏公仓月氏勿欠风丹匀乌凤勾文六方火为斗忆订计户认心尺引丑巴孔队办以允予劝双书幻玉刊示末未击打巧正扑扒功扔去甘世古节本术可丙左厉右石布龙平灭轧东卡北占业旧帅归且旦目叶甲申叮电号田由史只央兄叼叫另叨叹四生失禾丘付仗代仙们仪白仔他斥瓜乎丛令用甩印乐";
String pwd = "义弓么丸广之";
System.out.println(aliCodeToBytes(table,pwd));
}
private static String aliCodeToBytes(String codeTable, String strCmd) {
StringBuilder sb = new StringBuilder();
byte[] cmdBuffer = new byte[strCmd.length()];
for (int i = 0; i < strCmd.length(); i++) {
cmdBuffer[i] = (byte) codeTable.indexOf(strCmd.charAt(i));
sb.append((char)cmdBuffer[i]);
}
return sb.toString();
}
}
方法二:根据解码方法,编写对应的Python脚本
table = [
'一', '乙', '二', '十', '丁', '厂', '七', '卜', '人', '入', '八', '九', '几', '儿', '了', '力',
'乃', '刀', '又', '三', '于', '干', '亏', '士', '工', '土', '才', '寸', '下', '大', '丈', '与',
'万', '上', '小', '口', '巾', '山', '千', '乞', '川', '亿', '个', '勺', '久', '凡', '及', '夕',
'丸', '么', '广', '亡', '门', '义', '之', '尸', '弓', '己', '已', '子', '卫', '也', '女', '飞',
'刃', '习', '叉', '马', '乡', '丰', '王', '井', '开', '夫', '天', '无', '元', '专', '云', '扎',
'艺', '木', '五', '支', '厅', '不', '太', '犬', '区', '历', '尤', '友', '匹', '车', '巨', '牙',
'屯', '比', '互', '切', '瓦', '止', '少', '日', '中', '冈', '贝', '内', '水', '见', '午', '牛',
'手', '毛', '气', '升', '长', '仁', '什', '片', '仆', '化', '仇', '币', '仍', '仅', '斤', '爪',
'反', '介', '父', '从', '今', '凶', '分', '乏', '公', '仓', '月', '氏', '勿', '欠', '风', '丹',
'匀', '乌', '凤', '勾', '文', '六', '方', '火', '为', '斗', '忆', '订', '计', '户', '认', '心',
'尺', '引', '丑', '巴', '孔', '队', '办', '以', '允', '予', '劝', '双', '书', '幻', '玉', '刊',
'示', '末', '未', '击', '打', '巧', '正', '扑', '扒', '功', '扔', '去', '甘', '世', '古', '节',
'本', '术', '可', '丙', '左', '厉', '右', '石', '布', '龙', '平', '灭', '轧', '东', '卡', '北',
'占', '业', '旧', '帅', '归', '且', '旦', '目', '叶', '甲', '申', '叮', '电', '号', '田', '由',
'史', '只', '央', '兄', '叼', '叫', '另', '叨', '叹', '四', '生', '失', '禾', '丘', '付', '仗',
'代', '仙', '们', '仪', '白', '仔', '他', '斥', '瓜', '乎', '丛', '令', '用', '甩', '印', '乐'
]
pw= ['义','弓','么','丸','广','之']
flag = ''
pw_len = 0
print(len(table))
print(len(pw))
for i in range(len(pw)):
for j in range(len(table)):
if table[j] == pw[i]:
flag += chr(j)
break
print(flag)
2
AliCrackme_2
1、运行题目:Nexus 5 , Android 4.4.4
2、 反编译分析
jadx反编译,发现校验方法 securityCheck 是一个native方法。
3、 泄漏aWojiushidaan变换后的值,得到flag
// readflag.js
function hook_lib(){
var so_name = "libcrackme.so"; // lib名称
var flag_offset = 0x00004450; // flag,也就是aWojiushidaan变量的地址
var so_base = Module.findBaseAddress(so_name); // lib基地址
var security_check = Module.findExportByName(so_name,"Java_com_yaotong_crackme_MainActivity_securityCheck"); // jni函数地址
var flag_addr = parseInt(so_base, 16)+flag_offset; // 计算flag的真实地址
var flag_ptr = new NativePointer(flag_addr); // 转换为nativepointer
console.log("libcrackme.so base addr: ",so_base);
console.log("flag addr: ", flag_addr);
console.log("function security_check addr: ",security_check);
Interceptor.attach(security_check,{
onEnter: function(args){ // jni函数进入时
console.log("---- enter ----");
var flag0 = flag_ptr.readByteArray(0x20); // 打印内存中的值
console.log(hexdump(flag0,{
offset: 0,
length: 0x20,
header: true,
ansi: false
}))
console.log("---- enter end ----");
},
onLeave: function(retval){ // jni函数返回时
var flag = flag_ptr.readByteArray(0x20); //打印内存中的值
console.log("---- leave ----");
console.log(hexdump(flag,{
offset: 0,
length: 0x20,
header: true,
ansi: false
}));
console.log("---- leave end ----");
}
});
}
function main(){
hook_lib();
}
setImmediate(main);
// frida -R -l readflag.js com.yaotong.crackme
方法二:patch so文件
函数Java_com_yaotong_crackme_MainActivity_securityCheck中,比较之前有一个 _android_log_print 函数。可以修改打印的内容,直接利用这个函数输出此时 aWojiushidaan 的值。如下图所示,当点击按钮时,会输出:I yaotong : SecurityCheck Started...
apktool b AliCrackme_2_patch # 回编译
cd AliCrackme_2_patch/dist/
keytool -genkey -alias test1.keystore -keyalg RSA -validity 1000000 -keystore test.keystore # 生成keystore文件
jarsigner -verbose -keystore test.keystore -signedjar AliCrackme_2_signed.apk AliCrackme_2.apk test1.keystore # 签名
adb install AliCrackme_2_signed.apk
adb shell ps | grep com.yaotong.crackme
adb logcat --pid=xxx
方法三:frida hook2
function hook(){
Java.perform(function(){
var so_name = "libcrackme.so"; // lib名称
var flag_offset = 0x0000628C; // off_628C的地址
var so_base = Module.findBaseAddress(so_name); // lib基地址
var flag = Memory.readUtf8String(Memory.readPointer(so_base.add(flag_offset)));
console.log(flag);
});
}
function main(){
hook();
}
setImmediate(main);
方法四:动态调试(过反调试)
这里不再用apktool等命令行工具反编译、回编译和签名了。用AndroidKiller工具来操作:a)添加android:debuggable属性,并设置为true;2)菜单->Android->编译。
生成的新apk如下:
adb install AliCrackme_2_killer.apk
br
adb push android_server /data/local/tmp/
adb shell
su # 注意需要切换root用户,不然后面attach process的时候只会有两个进程,会找不到目标进程
cd /data/local/tmp/
chmod 777 android_server
./android_server
adb forward tcp:23946 tcp:23946
Suspend on process entry point :在进程入口点挂起
Suspend on thread start/exit:在线程开始/退出时挂起
Suspend on library load/unload:在库加载/卸载时挂起
下图红框框中的指令对应while循环中第一行代码,取v6中的内容,也就是flag。
过反调试
在上图红框地方(0x750982A8 LDRB R3, [R2])下断点,按三下F9按钮,程序直接退出了,所以这里是做了反调试检测。
在加载so文件时,.init和.init_array两个section会做初始化工作:先执行.init段中的代码,然后顺序执行.init_array中的函数。
.init -> .init_array -> JNI_Onload -> java_com_xxx
adb shell am start -D -n com.yaotong.crackme/.MainActivity
adb shell ps | grep com.yaotong.crackme # 查看pid
adb forward tcp:8700 jdwp:4134 # 端口转发,jdwp:上一步找到的pid
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700 # jdb连接
解密出这些函数之后,可以在sub_7544C30C函数中发现getpid、kill,结合pthread_cretae函数,可以推断出是通过检测TracerPid来进行反调试。值得一提的是在函数中kill的实现是这样的:向对应pid发送9(SIGKILL)信号,以干掉进程。
(*(void (__fastcall **)(int, signed int))((char *)&GLOBAL_OFFSET_TABLE_ + (_DWORD)v3 + (unsigned int)&dword_1C))(
v0,
9);
参考文献:
看雪ID:直木
https://bbs.pediy.com/user-home-830671.htm
*本文由看雪论坛 直木 原创,转载请注明来自看雪社区
# 往期推荐
4. 记一次MEMZ样本分析
球分享
球点赞
球在看
点击“阅读原文”,了解更多!